﻿using System;
using System.Collections.Generic;
using System.Drawing;
using System.IO;
using System.Text;
using Common.Utilities;
using PDFPARSERLib;

namespace PdfLib
{
    /// <summary>
    /// bookmark extraction
    /// </summary>
    public class PdfBookmarkReader
    {
        /// <summary>
        /// 
        /// </summary>
        /// <param name="pdfFilePath"></param>
        /// <returns></returns>
        public static List<PdfBookmark> GetAllBookmarks(string pdfFilePath)
        {
            int pageCount = PdfPropertyReader.GetPageCount(pdfFilePath);
            Dictionary<int, SizeF> pageSizes = new Dictionary<int, SizeF>();
            for (int i = 1; i <= pageCount; i++)
            {
                SizeF pageSize = PdfPropertyReader.GetPageSize(pdfFilePath, i);
                pageSizes.Add(i, pageSize);
            }

            List<PdfBookmark> bookmarks = new List<PdfBookmark>();
            PDFPARSERLib.Document pdfDoc = new PDFPARSERLib.Document();
            if (pdfDoc.Open(pdfFilePath, ""))
            {
                string lastPath = string.Empty;
                int lastDepth = 0;
                OutlineItem outline = pdfDoc.GetFirstOutlineItem();
                PdfBookmark lastBookmark = null;
                while (outline != null && outline.Dest !=null && 
                    pageSizes.ContainsKey(outline.Dest.PageNo))
                {
                    SizeF pageSize = pageSizes[outline.Dest.PageNo];
                    PdfBookmark bookmark = new PdfBookmark();
                    bookmark.BookmarkID = -1;
                    bookmark.FromPageNumber = outline.Dest.PageNo;
                    bookmark.FromPosition = new PointF(outline.Dest.Left, pageSize.Height - outline.Dest.Top);
                    if(bookmark.FromPosition.Y >= pageSize.Height)
                    {
                        bookmark.FromPageNumber = bookmark.FromPageNumber + 1;
                        bookmark.FromPosition = new PointF(bookmark.FromPosition.X, 0);
                    }
                    bookmark.ToPageNumber = pageCount;
                    bookmark.ToPosition = new PointF(pageSize.Width, pageSize.Height);
                    bookmark.Title = outline.Title;
                    int depth = pdfDoc.GetCurrentOutlineLevel();
                    if (string.IsNullOrEmpty(lastPath))
                    {
                        bookmark.Path = ToNodeName(0);
                    }
                    else if (depth == lastDepth)
                    {
                        string parentPath = GetParentPath(lastPath);
                        int currentIndex = GetSiblingIndex(lastPath) + 1;
                        if (string.IsNullOrEmpty(parentPath))
                        {
                            bookmark.Path = ToNodeName(currentIndex);
                        }
                        else
                        {
                            bookmark.Path = parentPath + "." + ToNodeName(currentIndex);
                        }
                    }
                    else if (depth > lastDepth)
                    {
                        int currentIndex = 0;
                        bookmark.Path = lastPath + "." + ToNodeName(currentIndex);
                    }
                    else // (depth <lastDepth)
                    {
                        int parentDepth = Math.Max(0, depth - 1);
                        string parentPath = GetParentPath(lastPath, parentDepth);
                        int currentIndex = GetLastSiblingIndex(parentPath, depth, bookmarks) + 1;
                        if (string.IsNullOrEmpty(parentPath))
                        {
                            bookmark.Path = ToNodeName(currentIndex);
                        }
                        else
                        {
                            bookmark.Path = parentPath + "." + ToNodeName(currentIndex);
                        }
                    }

                    lastDepth = depth;
                    lastPath = bookmark.Path;
                    bookmarks.Add(bookmark);

                    if (lastBookmark != null)
                    {
                        lastBookmark.ToPageNumber = bookmark.FromPageNumber;
                        lastBookmark.ToPosition = bookmark.FromPosition;
                    }
                    lastBookmark = bookmark;

                    outline = pdfDoc.GetNextOutlineItem(100, false);
                }

                pdfDoc.Close();
            }

            if (bookmarks == null || bookmarks.Count == 0)
                throw new Exception(string.Format("The pdf document {0} do not contain any bookmarks",
                                                  Path.GetFileName(pdfFilePath)));

            PdfBookmarkReader.CorrectBookmarkPositions(pdfFilePath, pageSizes, ref bookmarks);
            return bookmarks;
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="pdfFilePath"></param>
        /// <param name="title"></param>
        /// <returns></returns>
        public static PdfBookmark FindBookmarkByTitle(string pdfFilePath, string title)
        {
            List<PdfBookmark> allBookmarks = PdfBookmarkReader.GetAllBookmarks(pdfFilePath);
            foreach (PdfBookmark bookmark in allBookmarks)
            {
                string matchedStringInPage = string.Empty;
                StringUtil.LongestCommonSubsequence(
                    bookmark.Title, title, ref matchedStringInPage);
                int matchedStringLen1 = Math.Min(matchedStringInPage.Length, title.Length);
                int matchedStringLen2 = StringUtil.LongestCommonSubString(bookmark.Title, title);
                double score = (double)Math.Min(matchedStringLen1, matchedStringLen2) / title.Length;
                if (score > 0.95)
                {
                    return bookmark;
                }
            }
            return null;
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="pdfFilePath"></param>
        /// <param name="path"></param>
        /// <returns></returns>
        public static PdfBookmark FindBookmarkByPath(string pdfFilePath, string path)
        {
            List<PdfBookmark> allBookmarks = PdfBookmarkReader.GetAllBookmarks(pdfFilePath);
            foreach (PdfBookmark bookmark in allBookmarks)
            {
                if (bookmark.Path == path)
                    return bookmark;
            }
            return null;
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="pdfFilePath"></param>
        /// <param name="searchPatterns"></param>
        /// <returns></returns>
        public static PdfBookmark FindBookmark(string pdfFilePath, string[] searchPatterns)
        {
            List<PdfBookmark> allBookmarks = PdfBookmarkReader.GetAllBookmarks(pdfFilePath);
            foreach (string pattern in searchPatterns)
            {
                foreach (PdfBookmark bookmark in allBookmarks)
                {
                    string matchedStringInPage = string.Empty;
                    StringUtil.LongestCommonSubsequence(
                        bookmark.Title, pattern, ref matchedStringInPage);
                    int matchedStringLen1 = Math.Min(matchedStringInPage.Length, pattern.Length);
                    int matchedStringLen2 = StringUtil.LongestCommonSubString(bookmark.Title, pattern);
                    double score = (double)Math.Min(matchedStringLen1, matchedStringLen2) / pattern.Length;
                    if (score > 0.95)
                    {
                        return bookmark;
                    }
                }
            }

            allBookmarks.Sort();
            for (int i = 0; i < allBookmarks.Count; i++)
            {
                PdfBookmark bookmark = allBookmarks[i];
                List<PdfTextToken> tokens = PdfTextReader.ReadPageContentAsTokens(pdfFilePath, bookmark.FromPageNumber);
                List<PdfTextLine> lines = PdfTextReader.CreateLines(tokens);
                lines = PdfTextReader.MergeLines(lines, 8);
                lines.Sort();
                float matchedFontSize = float.MinValue;
                PointF matchedPos = bookmark.FromPosition;
                foreach (string pattern in searchPatterns)
                {
                    foreach (PdfTextLine line in lines)
                    {
                        string matchedStringInPage = string.Empty;
                        StringUtil.LongestCommonSubsequence(
                            line.LineContent, pattern, ref matchedStringInPage);
                        int matchedStringLen1 = Math.Min(matchedStringInPage.Length, pattern.Length);
                        int matchedStringLen2 = StringUtil.LongestCommonSubString(line.LineContent, pattern);
                        double score = (double)Math.Min(matchedStringLen1, matchedStringLen2) / pattern.Length;
                        if (score > 0.95)
                        {
                            float fontSize = line.TextTokens[0].FontSize;
                            if (fontSize > matchedFontSize + 1)
                            {
                                matchedFontSize = fontSize;
                                matchedPos = line.TextTokens[0].Position;
                            }
                        }
                    }

                }
                if (matchedFontSize >= 8)
                {
                    bookmark.FromPosition = matchedPos;
                    return bookmark;
                }
            }

            return null;
        }

        private static void CorrectBookmarkPositions(string pdfFilePath,
            Dictionary<int, SizeF> pageSizes,
            ref List<PdfBookmark> bookmarks)
        {
            bookmarks.Sort();
            Dictionary<int, List<PdfTextToken>> txtTokensByPages = new Dictionary<int, List<PdfTextToken>>();
            int pageCount = PdfPropertyReader.GetPageCount(pdfFilePath);
            for (int pageNum = 1; pageNum <= pageCount; pageNum++)
            {
                List<PdfTextToken> txtTokens = PdfTextReader.ReadPageContentAsTokens(pdfFilePath, pageNum);
                txtTokensByPages.Add(pageNum, txtTokens);
            }

            // update begining positions, assuming bookmark title have the largest font size
            // and bookmark title is within one line
            for (int i = 0; i < bookmarks.Count; i++)
            {
                PdfBookmark bookmark = bookmarks[i];
                int fromPageNum = bookmark.FromPageNumber;
                List<PdfTextToken> tokens = txtTokensByPages[fromPageNum];
                if (tokens != null && tokens.Count > 0)
                {
                    PointF posFrom = bookmark.FromPosition;
                    float bookmarkFontSize = float.MinValue;
                    List<PdfTextLine> lines = PdfTextReader.CreateLines(tokens);
                    lines = PdfTextReader.MergeLines(lines, 8);
                    lines.Sort();
                    double bestScore = 0;
                    string searchPattern = bookmark.Title;
                    if (bookmark.SearchPatterns != null && bookmark.SearchPatterns.Length > 0)
                    {
                        foreach (string pattern in bookmark.SearchPatterns)
                        {
                            foreach (PdfTextLine line in lines)
                            {
                                string matchedStringInPage = string.Empty;
                                StringUtil.LongestCommonSubsequence(
                                    line.LineContent, pattern, ref matchedStringInPage);
                                int matchedStringLen = Math.Min(matchedStringInPage.Length, searchPattern.Length);
                                double score = (double)matchedStringLen / searchPattern.Length;
                                if (score > bestScore)
                                {
                                    searchPattern = pattern;
                                    bestScore = score;
                                }
                            }
                        }
                    }
                    foreach (PdfTextLine line in lines)
                    {
                        string matchedStringInPage = string.Empty;

                        StringUtil.LongestCommonSubsequence(
                            line.LineContent, searchPattern, ref matchedStringInPage);
                        int matchedStringLen1 = Math.Min(matchedStringInPage.Length, searchPattern.Length);
                        int matchedStringLen2 = StringUtil.LongestCommonSubString(line.LineContent, searchPattern);
                        double score = (double)Math.Min(matchedStringLen1, matchedStringLen2) / searchPattern.Length;
                        if (score > 0.9)
                        {
                            float fontSize = line.TextTokens[0].FontSize;
                            if (fontSize > bookmarkFontSize + 1)
                            {
                                bookmarkFontSize = fontSize;
                                posFrom = line.TextTokens[0].Position;
                            }
                        }
                    }
                    bookmark.FromPosition = posFrom;
                }
            }

            // update end position of each bookmark to the start position of its next sibling bookmark
            for (int i = 0; i < bookmarks.Count; i++)
            {
                PdfBookmark bookmark = bookmarks[i];
                PdfBookmark nextSibBookmark = PdfBookmarkReader.GetNextBookmark(bookmarks, bookmark);
                if (nextSibBookmark != null)
                {
                    if (nextSibBookmark.FromPosition.Y <= 0)
                    {
                        bookmark.ToPageNumber = nextSibBookmark.FromPageNumber - 1;
                        int toPageNum = Math.Max(1, nextSibBookmark.FromPageNumber - 1);
                        bookmark.ToPosition = pageSizes[toPageNum].ToPointF();
                    }
                    else
                    {
                        bookmark.ToPageNumber = nextSibBookmark.FromPageNumber;
                        bookmark.ToPosition = nextSibBookmark.FromPosition;
                    }
                }
                else
                {
                    bookmark.ToPageNumber = pageCount;
                    bookmark.ToPosition = pageSizes[pageCount].ToPointF();
                }
            }
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="allBookmarks"></param>
        /// <param name="currentBookmark"></param>
        /// <returns></returns>
        public static PdfBookmark GetNextBookmark(List<PdfBookmark> allBookmarks, PdfBookmark currentBookmark)
        {
            allBookmarks.Sort();
            for (int i = 0; i < allBookmarks.Count; i++)
            {
                if (allBookmarks[i].Path == currentBookmark.Path)
                {
                    string[] currentPathNodes = StringUtil.Split(currentBookmark.Path, ".");
                    for (int k = i + 1; k < allBookmarks.Count; k++)
                    {
                        string[] pathNodes = StringUtil.Split(allBookmarks[k].Path, ".");
                        if (currentPathNodes.Length < pathNodes.Length)
                            continue;
                        else
                            return allBookmarks[k];
                    }
                    break;
                }
            }
            // current bookmark is the last bookmark
            return null;
        }

        private static int GetLastSiblingIndex(string parentPath, int siblingDepth,
            List<PdfBookmark> bookmarks)
        {
            int lastSibIndex = -1;
            bookmarks.Sort();
            foreach (PdfBookmark bookmark in bookmarks)
            {
                if (bookmark.Path.StartsWith(parentPath))
                {
                    int depth = bookmark.Path.Split(new char[] { '.' }).Length;
                    if (depth == siblingDepth)
                    {
                        string nodeName = bookmark.Path.Substring(bookmark.Path.LastIndexOf(".") + 1);
                        lastSibIndex = ToNodeIndex(nodeName);
                    }
                }
            }
            return lastSibIndex;
        }

        private static int GetSiblingIndex(string sibPath)
        {
            if (sibPath.LastIndexOf(".") > 0)
            {
                string nodeName = sibPath.Substring(sibPath.LastIndexOf(".") + 1);
                return ToNodeIndex(nodeName);
            }
            else
            {
                return ToNodeIndex(sibPath);
            }
        }

        private static string GetParentPath(string path)
        {
            if (path.IndexOf(".") > 0)
            {
                return path.Substring(0, path.LastIndexOf("."));
            }
            else
                return "";
        }

        private static string ToNodeName(int nodeIndex)
        {
            string nodeStr = nodeIndex.ToString();
            if (nodeIndex < 10)
            {
                nodeStr = "00" + nodeStr;
            }
            else if (nodeIndex < 100)
            {
                nodeStr = "0" + nodeStr;
            }
            return nodeStr;
        }

        private static int ToNodeIndex(string nodeName)
        {
            return int.Parse(nodeName);
        }

        private static string GetParentPath(string currentPath, int parentDepth)
        {
            string[] nodeNames = currentPath.Split(new char[] { '.' });
            string parentPath = string.Empty;
            for (int i = 0; i < parentDepth; i++)
            {
                parentPath += nodeNames[i];
                if (i < parentDepth - 1)
                    parentPath += ".";
            }
            return parentPath;
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="pdfFilePath"></param>
        /// <param name="bookmark"></param>
        /// <returns></returns>
        public static string ReadBookmarkContent(string pdfFilePath, PdfBookmark bookmark)
        {
            return
                PdfTextReader.GetContent(pdfFilePath, bookmark.FromPageNumber, bookmark.FromPosition,
                                             bookmark.ToPageNumber, bookmark.ToPosition);
        }
    }
}
